MEV Attacks¶

Imports¶

In [2]:
import pandas as pd
import plotly.io as pio
from sqlalchemy import create_engine

from mev_attacks import utils
import requests

pd.options.plotting.backend = "plotly"
pio.renderers.default="notebook"
pio.templates.default = "seaborn"
In [3]:
DB_AVAIL = False
if DB_AVAIL:
    user = "postgres"
    password="password"
    host = "localhost"
    engine   = create_engine(f'postgresql+psycopg2://{user}:{password}@{host}/mev_inspect', pool_recycle=3600)
    conn = engine.connect();

Sandwich Attack¶

Description¶

A sandwich attack involves executing transactions immediately before and immediately after a swap transaction on a decentralized exchange that sets prices according to a known formula. Many of the largest decentralized exchanges utilize a constant product formula such that the product of the supply of each token in a pool must remain constant.

For example, if there were a pool of a pair of tokens A and B each with an initial supply of 10, the product would need to remain constant at 100. To determine the number of B tokens that would be received for 1 A token, you would take 100 divided by 11, the new supply of A, to determine that after the transaction there needs to be 9.090909 B tokens in order to keep the product of the supplies constant at 100. Therefore, you could swap 1 A token for 0.90909 B tokens.

A B k / price
$t_0$ 10.0000 10.0000 100.0000
$user$ 1.0000 -0.9091 0.9091
$t_3$ 11.0000 9.0909 100.0000

Since transactions are written together in blocks, the final amount received for an individual transaction depends on the other transactions in the block that get executed before it. In order to protect users from unlimited exposure to changes in their received price, one of the parameters they specify is the amount of slippage they will suffer before their transaction will simply not be executed.

The sandwich attack will execute transactions on either side of a swap transaction to capture the maximum allowable slippage. The default slippage for Uniswap is 0.5%, but it can be set to anything. To illustrate the nature of the attack, assume that the allowable slippage in the above transaction was specified to be 10%, meaning that as few as 0.8181 B tokens could be received for 1 A token and the transaction would still be executed.

To execute an attack to capture the maximum possible slippage of 0.0909 a searcher would first determine what transaction they need to execute such that the supply of A and B tokens in the pool when the user’s transaction is executed results in the user receiving 0.8181 B tokens. Then after the user’s transaction, the searcher executes another transaction in the opposite direction that leaves the supply in the pools where it would have been without the attack, netting them the slippage at the user’s expense.

$$ \begin{align*} Let A_1 &= \text{the supply of token A after the attacker's initial transaction} \\ Let B_1 &= \text{the supply of token B after the attacker's initial transaction} \\ \end{align*} $$

and in order for $A * B = 100$ to be true,

$$ \begin{align*} (A_1 + 1.0000) * (B_1 - 0.8181) &= 100 \\ (A_1 + 1.0000) * (\frac{100}{A_1} - 0.8181) &= 100 \\ A_1 = 10.5667 \end{align*} $$

which means the swap the searcher executes initially is for 0.5363 B tokens in exchange for 0.5667 A tokens. After the user's transaction the searcher swaps their 0.5667 A tokens for 0.4454 B tokens to net 0.0909 B tokens from the sandwich attack.

A B k / price
$t_0$ 10.0000 10.0000 100.0000
$pre$ 0.5667 -0.5363 0.9463
$t_1$ 10.5667 9.4637 100.0000
$user$ 1.0000 -0.8182 -0.8182
$t_2$ 11.5667 8.6455 100.0000
$post$ -0.5667 0.4454 0.7860
$t_3$ 11.0000 9.0909 100.0000
$profit$ 0.0909

Examples¶

In [4]:
if DB_AVAIL:
    df_sandwich = pd.read_sql("SELECT * from sandwiches", conn)
    df_sandwich.to_csv("data/sandwiches.csv", index=False)
else:
    df_sandwich = pd.read_csv("data/sandwiches.csv")
In [5]:
df_sandwich.head()
Out[5]:
id created_at block_number sandwicher_address frontrun_swap_transaction_hash frontrun_swap_trace_address backrun_swap_transaction_hash backrun_swap_trace_address
0 783a005e-b3fa-401c-9a38-bfe13e701346 2022-01-13 21:34:44.042851 12634570.0 0x00000000003b3cc22af3ae1eac0440bcee416b40 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... [1] 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... [1]
1 4b83d760-b597-457a-84a6-ca017a8ea444 2022-01-13 21:34:44.042851 12634575.0 0x000000000035b5e5ad9019092c665357240f594e 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... [1] 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... [1]
2 9929faff-c29a-41ff-a08d-3e8868f1bcb3 2022-01-13 21:34:44.042851 12634579.0 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... [1] 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... [1]
3 28ade08a-f5f3-4554-91ad-d4a550bc14c7 2022-01-13 21:34:44.417610 12634558.0 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... [1] 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... [1]
4 fccf0e9a-8745-43fa-801f-e3551e824ff0 2022-01-13 21:34:44.417610 12634558.0 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... [2] 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... [4]
In [6]:
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].frontrun_swap_transaction_hash}")
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].backrun_swap_transaction_hash}")
https://etherscan.io/tx/0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec42ddf7fc4f880bc3def16
https://etherscan.io/tx/0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6b2d34e39e0d1bf7c64b3

The front run transaction was to swap 87.0484 WETH for 416,650.9839 KEEP and the back run was to swap the 416,650.9839 KEEP back for 88.3490 WETH, for a gross profit of 1.2265 WETH.

Check Profit Calculation¶

Here we check the simple profit sandwich profit calculation for a sequence of 65,000 blocks from 2021-06-14 to 2021-06-24.

In [7]:
df_sandwich
Out[7]:
id created_at block_number sandwicher_address frontrun_swap_transaction_hash frontrun_swap_trace_address backrun_swap_transaction_hash backrun_swap_trace_address
0 783a005e-b3fa-401c-9a38-bfe13e701346 2022-01-13 21:34:44.042851 12634570.0 0x00000000003b3cc22af3ae1eac0440bcee416b40 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... [1] 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... [1]
1 4b83d760-b597-457a-84a6-ca017a8ea444 2022-01-13 21:34:44.042851 12634575.0 0x000000000035b5e5ad9019092c665357240f594e 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... [1] 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... [1]
2 9929faff-c29a-41ff-a08d-3e8868f1bcb3 2022-01-13 21:34:44.042851 12634579.0 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... [1] 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... [1]
3 28ade08a-f5f3-4554-91ad-d4a550bc14c7 2022-01-13 21:34:44.417610 12634558.0 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... [1] 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... [1]
4 fccf0e9a-8745-43fa-801f-e3551e824ff0 2022-01-13 21:34:44.417610 12634558.0 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... [2] 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... [4]
... ... ... ... ... ... ... ... ...
41605 6ac12623-f210-436b-8f39-0b2369b92681 2022-01-14 09:20:02.083052 12653919.0 0x000000000000cb53d776774284822b1298ade47f 0x556f35176e7fcbdfdaf499ab6aa5dc2e3a907e24e0bc... [1] 0x85e5d66cd70168690c74646feb96c8820a6555cc695f... [1]
41606 ca838a0f-f210-45ee-8c2e-16834fa6f1b1 2022-01-14 09:20:02.083052 12653919.0 0x0000000057a888b5dc0a81f02c6f5c3b7d16b183 0x6f60f651175c000f2df435245d6caa0030e6afbad0a2... [1] 0xb63968fcb99e72b0549fda055f6b7d515e704fd9d7b6... [1]
41607 8f869e6f-580c-4604-801a-078e0aa2dd02 2022-01-14 09:20:54.590911 12654202.0 0x3dad8cf200799f82fd8eb68f608220d8f3ebf8de 0xa20e43558a9672acbfccc32faee87d53b485939e8845... [2] 0x8e939a3b41daaf119db9118cc8ce1561a73aa8713574... [4]
41608 a2cf19ae-c976-4990-b55e-272bbc25f734 2022-01-14 09:20:54.590911 12654203.0 0x7a250d5630b4cf539739df2c5dacb4c659f2488d 0x741ea5513dfc105ccaeb6b9ae66bde953d06626c31da... [3] 0x539caed60bbc661b0116068fb71fdac6940dfe767aff... [3]
41609 d3d06e83-9ea5-4e9d-93c4-795f8cd2349e 2022-01-14 09:20:54.590911 12654204.0 0x00000000003b3cc22af3ae1eac0440bcee416b40 0x3b5b852b22b3861136f2eed1b9ce369e48ecbcc22116... [1] 0xf679c046d96b8603758c9ec443dbf2be8aeb4029769c... [1]

41610 rows × 8 columns

There are duplicates in the swaps table. This is a pull of 10,000 and ~4,500 have at least one duplicate.

In [8]:
if DB_AVAIL:
    df_swap = pd.read_sql("SELECT * FROM swaps LIMIT 10000", conn)
    df_swap.to_csv("data/swaps.csv", index=False)
else:
    df_swap = pd.read_csv("data/swaps.csv")
df_swap.columns
Out[8]:
Index(['created_at', 'abi_name', 'transaction_hash', 'block_number',
       'protocol', 'contract_address', 'from_address', 'to_address',
       'token_in_address', 'token_in_amount', 'token_out_address',
       'token_out_amount', 'trace_address', 'error', 'transaction_position'],
      dtype='object')
In [9]:
df_swap.transaction_hash.value_counts().hist()
In [10]:
if DB_AVAIL:
    print(pd.read_sql("SELECT min(block_number) min_block, max(block_number) max_block from swaps where block_number < 13998686", conn).astype(int))
In [11]:
q = """
    SELECT 
        id,
        frontrun_swap_transaction_hash,
        backrun_swap_transaction_hash,
        swap_front.transaction_hash front_transaction_hash,
        swap_front.token_in_amount front_token_in_amount,
        swap_front.token_out_amount front_token_out_amount,
        swap_back.token_in_amount back_token_in_amount,
        swap_back.token_out_amount back_token_out_amount
    FROM sandwiches
    LEFT JOIN swaps swap_front ON sandwiches.frontrun_swap_transaction_hash = swap_front.transaction_hash
    LEFT JOIN swaps swap_back ON sandwiches.backrun_swap_transaction_hash = swap_back.transaction_hash
"""
In [12]:
if DB_AVAIL:
    df_profit = pd.read_sql(q, conn)
    df_profit.to_csv("data/profit.csv", index=False)
else:
    df_profit = pd.read_csv("data/profit.csv")

df_profit.id.value_counts().hist()
In [13]:
df_profit["profit_front_token_in"] = df_profit.back_token_out_amount - df_profit.front_token_in_amount
df_profit["profit_front_token_out"] = df_profit.back_token_in_amount - df_profit.front_token_out_amount

Filtering out duplicates as first pass to check profit calcs - need to figure out why dupes exist and how to properly filter out.

In [14]:
df_profit = df_profit.drop_duplicates("id")
df_profit["profit_bin_front_out"] = pd.qcut(df_profit.profit_front_token_out / 1e18, 50, duplicates="drop")
df_profit["profit_bin_front_in"] = pd.qcut(df_profit.profit_front_token_in / 1e18, 50, duplicates="drop")

In theory, the front token out should exactly equal the back token in, with the actual profit being the delta between the front token in and the back token out. Here we check to see how often the front token out profit deviates from zero.

In [15]:
df_profit_freq = df_profit.groupby("profit_bin_front_out").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token Out Profit Freq")

Just to check to see whether profit might be being taken on the other side, we check the profit on the front token in.

In [16]:
df_profit_freq = df_profit.groupby("profit_bin_front_in").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token In Profit Freq")